diff --git a/landing/investor-profile.react.js b/landing/investor-profile.react.js index 8dcc316c7..d240dec05 100644 --- a/landing/investor-profile.react.js +++ b/landing/investor-profile.react.js @@ -1,115 +1,107 @@ // @flow import { faTwitter, faLinkedin } from '@fortawesome/free-brands-svg-icons'; import { faGlobe } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import classNames from 'classnames'; import * as React from 'react'; import css from './investor-profile.css'; type Props = { +name: string, +description: string, +involvement?: string, +imageURL: string, +onClick: () => void, +isModalActive?: boolean, +website?: string, +twitterHandle?: string, +linkedinHandle?: string, }; function InvestorProfile(props: Props): React.Node { const { name, description, involvement, imageURL, onClick, isModalActive, website, twitterHandle, linkedinHandle, } = props; - const profileContainerClassName = React.useMemo( - () => - classNames({ - [css.profile]: true, - [css.profileModal]: isModalActive, - }), - [isModalActive], - ); + const profileContainerClassName = classNames({ + [css.profile]: true, + [css.profileModal]: isModalActive, + }); - const descriptionClassName = React.useMemo( - () => - classNames({ - [css.description]: true, - [css.descriptionModal]: isModalActive, - }), - [isModalActive], - ); + const descriptionClassName = classNames({ + [css.description]: true, + [css.descriptionModal]: isModalActive, + }); const stopPropagation = React.useCallback(e => e.stopPropagation(), []); let websiteIcon; if (website) { websiteIcon = ( ); } let twitterIcon; if (twitterHandle) { twitterIcon = ( ); } let linkedinIcon; if (linkedinHandle) { linkedinIcon = ( ); } return ( {name} {description} {involvement} {websiteIcon} {twitterIcon} {linkedinIcon} ); } export default InvestorProfile; diff --git a/web/chat/chat-thread-composer.react.js b/web/chat/chat-thread-composer.react.js index 974919d08..773a54ae3 100644 --- a/web/chat/chat-thread-composer.react.js +++ b/web/chat/chat-thread-composer.react.js @@ -1,187 +1,183 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import { userSearchIndexForPotentialMembers } from 'lib/selectors/user-selectors'; import { getPotentialMemberItems } from 'lib/shared/search-utils'; import { threadIsPending } from 'lib/shared/thread-utils'; import type { AccountUserInfo, UserListItem } from 'lib/types/user-types'; import Button from '../components/button.react'; import Label from '../components/label.react'; import Search from '../components/search.react'; import type { InputState } from '../input/input-state'; import { updateNavInfoActionType } from '../redux/action-types'; import { useSelector } from '../redux/redux-utils'; import SWMansionIcon from '../SWMansionIcon.react'; import css from './chat-thread-composer.css'; type Props = { +userInfoInputArray: $ReadOnlyArray, +otherUserInfos: { [id: string]: AccountUserInfo }, +threadID: string, +inputState: InputState, }; type ActiveThreadBehavior = | 'reset-active-thread-if-pending' | 'keep-active-thread'; function ChatThreadComposer(props: Props): React.Node { const { userInfoInputArray, otherUserInfos, threadID, inputState } = props; const [usernameInputText, setUsernameInputText] = React.useState(''); const dispatch = useDispatch(); const userSearchIndex = useSelector(userSearchIndexForPotentialMembers); const userInfoInputIDs = React.useMemo( () => userInfoInputArray.map(userInfo => userInfo.id), [userInfoInputArray], ); const userListItems = React.useMemo( () => getPotentialMemberItems( usernameInputText, otherUserInfos, userSearchIndex, userInfoInputIDs, ), [usernameInputText, otherUserInfos, userSearchIndex, userInfoInputIDs], ); const onSelectUserFromSearch = React.useCallback( (id: string) => { const selectedUserIDs = userInfoInputArray.map(user => user.id); dispatch({ type: updateNavInfoActionType, payload: { selectedUserList: [...selectedUserIDs, id], }, }); setUsernameInputText(''); }, [dispatch, userInfoInputArray], ); const onRemoveUserFromSelected = React.useCallback( (id: string) => { const selectedUserIDs = userInfoInputArray.map(user => user.id); if (!selectedUserIDs.includes(id)) { return; } dispatch({ type: updateNavInfoActionType, payload: { selectedUserList: selectedUserIDs.filter(userID => userID !== id), }, }); }, [dispatch, userInfoInputArray], ); const userSearchResultList = React.useMemo(() => { if ( !userListItems.length || (!usernameInputText && userInfoInputArray.length) ) { return null; } return ( {userListItems.map((userSearchResult: UserListItem) => ( onSelectUserFromSearch(userSearchResult.id)} className={css.searchResultsButton} > {userSearchResult.username} {userSearchResult.alertTitle} ))} ); }, [ onSelectUserFromSearch, userInfoInputArray.length, userListItems, usernameInputText, ]); const hideSearch = React.useCallback( (threadBehavior: ActiveThreadBehavior = 'keep-active-thread') => { dispatch({ type: updateNavInfoActionType, payload: { chatMode: 'view', activeChatThreadID: threadBehavior === 'keep-active-thread' || !threadIsPending(threadID) ? threadID : null, }, }); }, [dispatch, threadID], ); const onCloseSearch = React.useCallback(() => { hideSearch('reset-active-thread-if-pending'); }, [hideSearch]); const tagsList = React.useMemo(() => { if (!userInfoInputArray?.length) { return null; } const labels = userInfoInputArray.map(user => { return ( onRemoveUserFromSelected(user.id)}> {user.username} ); }); return {labels}; }, [userInfoInputArray, onRemoveUserFromSelected]); React.useEffect(() => { if (!inputState) { return; } inputState.registerSendCallback(hideSearch); return () => inputState.unregisterSendCallback(hideSearch); }, [hideSearch, inputState]); - const threadSearchContainerStyles = React.useMemo( - () => - classNames(css.threadSearchContainer, { - [css.fullHeight]: !userInfoInputArray.length, - }), - [userInfoInputArray.length], - ); + const threadSearchContainerStyles = classNames(css.threadSearchContainer, { + [css.fullHeight]: !userInfoInputArray.length, + }); return ( {tagsList} {userSearchResultList} ); } export default ChatThreadComposer; diff --git a/web/chat/chat-thread-list-item.react.js b/web/chat/chat-thread-list-item.react.js index 2b878b41c..4cea476aa 100644 --- a/web/chat/chat-thread-list-item.react.js +++ b/web/chat/chat-thread-list-item.react.js @@ -1,179 +1,163 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import type { ChatThreadItem } from 'lib/selectors/chat-selectors'; import { useAncestorThreads } from 'lib/shared/ancestor-threads'; import { shortAbsoluteDate } from 'lib/utils/date-utils'; import { useSelector } from '../redux/redux-utils'; import { useOnClickThread, useThreadIsActive, } from '../selectors/thread-selectors'; import SWMansionIcon from '../SWMansionIcon.react'; import ChatThreadListItemMenu from './chat-thread-list-item-menu.react'; import ChatThreadListSeeMoreSidebars from './chat-thread-list-see-more-sidebars.react'; import ChatThreadListSidebar from './chat-thread-list-sidebar.react'; import css from './chat-thread-list.css'; import MessagePreview from './message-preview.react'; type Props = { +item: ChatThreadItem, }; function ChatThreadListItem(props: Props): React.Node { const { item } = props; const { threadInfo, lastUpdatedTimeIncludingSidebars, mostRecentNonLocalMessage, mostRecentMessageInfo, } = item; const { id: threadID, currentUser } = threadInfo; const ancestorThreads = useAncestorThreads(threadInfo); const timeZone = useSelector(state => state.timeZone); const lastActivity = shortAbsoluteDate( lastUpdatedTimeIncludingSidebars, timeZone, ); const active = useThreadIsActive(threadID); const isCreateMode = useSelector( state => state.navInfo.chatMode === 'create', ); const onClick = useOnClickThread(item.threadInfo); const selectItemIfNotActiveCreation = React.useCallback( (event: SyntheticEvent) => { if (!isCreateMode || !active) { onClick(event); } }, [isCreateMode, active, onClick], ); - const containerClassName = React.useMemo( - () => - classNames({ - [css.thread]: true, - [css.activeThread]: active, - }), - [active], - ); + const containerClassName = classNames({ + [css.thread]: true, + [css.activeThread]: active, + }); const { unread } = currentUser; - const titleClassName = React.useMemo( - () => - classNames({ - [css.title]: true, - [css.unread]: unread, - }), - [unread], - ); - const lastActivityClassName = React.useMemo( - () => - classNames({ - [css.lastActivity]: true, - [css.unread]: unread, - [css.dark]: !unread, - }), - [unread], - ); + const titleClassName = classNames({ + [css.title]: true, + [css.unread]: unread, + }); + const lastActivityClassName = classNames({ + [css.lastActivity]: true, + [css.unread]: unread, + [css.dark]: !unread, + }); - const breadCrumbsClassName = React.useMemo( - () => - classNames(css.breadCrumbs, { - [css.unread]: unread, - }), - [unread], - ); + const breadCrumbsClassName = classNames(css.breadCrumbs, { + [css.unread]: unread, + }); let unreadDot; if (unread) { unreadDot = ; } const { color } = item.threadInfo; const colorSplotchStyle = React.useMemo( () => ({ backgroundColor: `#${color}` }), [color], ); const sidebars = item.sidebars.map((sidebarItem, index) => { if (sidebarItem.type === 'sidebar') { const { type, ...sidebarInfo } = sidebarItem; return ( 0} key={sidebarInfo.threadInfo.id} /> ); } else if (sidebarItem.type === 'seeMore') { return ( ); } else { return ; } }); const ancestorPath = ancestorThreads.map((thread, idx) => { const isNotLast = idx !== ancestorThreads.length - 1; const chevron = isNotLast && ( ); return ( {thread.uiName} {chevron} ); }); return ( <> {unreadDot} {ancestorPath} {threadInfo.uiName} {lastActivity} {sidebars} > ); } export default ChatThreadListItem; diff --git a/web/chat/message-tooltip.react.js b/web/chat/message-tooltip.react.js index 44ceb8172..eb3661622 100644 --- a/web/chat/message-tooltip.react.js +++ b/web/chat/message-tooltip.react.js @@ -1,115 +1,111 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import { type MessageTooltipAction } from '../utils/tooltip-utils'; import { tooltipButtonStyle, tooltipLabelStyle, tooltipStyle, } from './chat-constants'; import css from './message-tooltip.css'; type MessageTooltipProps = { +actions: $ReadOnlyArray, +messageTimestamp: string, +alignment?: 'left' | 'center' | 'right', }; function MessageTooltip(props: MessageTooltipProps): React.Node { const { actions, messageTimestamp, alignment = 'left' } = props; const [activeTooltipLabel, setActiveTooltipLabel] = React.useState(); const messageActionButtonsContainerClassName = classNames( css.messageActionContainer, css.messageActionButtons, ); const messageTooltipButtonStyle = React.useMemo(() => tooltipButtonStyle, []); const tooltipButtons = React.useMemo(() => { if (!actions || actions.length === 0) { return null; } const buttons = actions.map(({ label, onClick, actionButtonContent }) => { const onMouseEnter = () => { setActiveTooltipLabel(label); }; const onMouseLeave = () => setActiveTooltipLabel(oldLabel => label === oldLabel ? null : oldLabel, ); return ( {actionButtonContent} ); }); return ( {buttons} ); }, [ actions, messageActionButtonsContainerClassName, messageTooltipButtonStyle, ]); const messageTooltipLabelStyle = React.useMemo(() => tooltipLabelStyle, []); const messageTooltipTopLabelStyle = React.useMemo( () => ({ height: `${tooltipLabelStyle.height + 2 * tooltipLabelStyle.padding}px`, }), [], ); const tooltipLabel = React.useMemo(() => { if (!activeTooltipLabel) { return null; } return ( {activeTooltipLabel} ); }, [activeTooltipLabel, messageTooltipLabelStyle]); const tooltipTimestamp = React.useMemo(() => { if (!messageTimestamp) { return null; } return ( {messageTimestamp} ); }, [messageTimestamp, messageTooltipLabelStyle]); const messageTooltipContainerStyle = React.useMemo(() => tooltipStyle, []); - const containerClassNames = React.useMemo( - () => - classNames(css.messageTooltipContainer, { - [css.leftTooltipAlign]: alignment === 'left', - [css.centerTooltipAlign]: alignment === 'center', - [css.rightTooltipAlign]: alignment === 'right', - }), - [alignment], - ); + const containerClassNames = classNames(css.messageTooltipContainer, { + [css.leftTooltipAlign]: alignment === 'left', + [css.centerTooltipAlign]: alignment === 'center', + [css.rightTooltipAlign]: alignment === 'right', + }); return ( {tooltipLabel} {tooltipButtons} {tooltipTimestamp} ); } export default MessageTooltip; diff --git a/web/components/enum-settings-option-info.react.js b/web/components/enum-settings-option-info.react.js index d36a464b3..088785b6f 100644 --- a/web/components/enum-settings-option-info.react.js +++ b/web/components/enum-settings-option-info.react.js @@ -1,49 +1,45 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; import SWMansionIcon from '../SWMansionIcon.react'; import css from './enum-settings-option-info.css'; type Props = { +optionSelected: boolean, +valid: boolean, +styleStatementBasedOnValidity: boolean, +children: React.Node, }; function EnumSettingsOptionInfo(props: Props): React.Node { const { optionSelected, valid, styleStatementBasedOnValidity, children, } = props; - const optionInfoClasses = React.useMemo( - () => - classnames({ - [css.optionInfo]: true, - [css.optionInfoInvalid]: styleStatementBasedOnValidity && !valid, - [css.optionInfoInvalidSelected]: - styleStatementBasedOnValidity && !valid && optionSelected, - }), - [styleStatementBasedOnValidity, valid, optionSelected], - ); + const optionInfoClasses = classnames({ + [css.optionInfo]: true, + [css.optionInfoInvalid]: styleStatementBasedOnValidity && !valid, + [css.optionInfoInvalidSelected]: + styleStatementBasedOnValidity && !valid && optionSelected, + }); const icon = React.useMemo(() => { if (!styleStatementBasedOnValidity) { return null; } return ; }, [styleStatementBasedOnValidity, valid]); return ( {icon} {children} ); } export default EnumSettingsOptionInfo; diff --git a/web/components/enum-settings-option.react.js b/web/components/enum-settings-option.react.js index 345226593..b844c1fa4 100644 --- a/web/components/enum-settings-option.react.js +++ b/web/components/enum-settings-option.react.js @@ -1,102 +1,98 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; import Checkbox from './checkbox.react'; import EnumSettingsOptionInfo from './enum-settings-option-info.react.js'; import css from './enum-settings-option.css'; import Radio from './radio.react'; const iconPositionClassnames = { top: css.optionIconTop, center: css.optionIconCenter, bottom: css.optionIconBottom, }; type InputType = 'radio' | 'checkbox'; type IconPosition = $Keys; type Props = { +selected: boolean, +onSelect: () => void, +disabled?: boolean, +icon: React.Node, +title: string, +type?: InputType, +iconPosition?: IconPosition, +statements: $ReadOnlyArray<{ +statement: string, +isStatementValid: boolean, +styleStatementBasedOnValidity: boolean, }>, }; function EnumSettingsOption(props: Props): React.Node { const { icon, title, statements, selected, onSelect, disabled = false, type = 'radio', iconPosition = 'center', } = props; const descriptionItems = React.useMemo( () => statements.map( ({ statement, isStatementValid, styleStatementBasedOnValidity }) => ( {statement} ), ), [selected, statements], ); const inputIcon = React.useMemo(() => { if (disabled) { return null; } else if (type === 'checkbox') { return ; } else if (type === 'radio') { return ; } }, [disabled, type, selected]); - const optionContainerClasses = React.useMemo( - () => - classnames(css.optionContainer, { - [css.optionContainerSelected]: selected, - }), - [selected], - ); + const optionContainerClasses = classnames(css.optionContainer, { + [css.optionContainerSelected]: selected, + }); - const optionIconClasses = React.useMemo( - () => classnames(css.optionIcon, iconPositionClassnames[iconPosition]), - [iconPosition], + const optionIconClasses = classnames( + css.optionIcon, + iconPositionClassnames[iconPosition], ); return ( {icon} {title} {descriptionItems} {inputIcon} ); } export default EnumSettingsOption; diff --git a/web/components/menu.react.js b/web/components/menu.react.js index 3d968ba44..292eacd2a 100644 --- a/web/components/menu.react.js +++ b/web/components/menu.react.js @@ -1,119 +1,115 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; import { useRenderMenu } from '../menu-provider.react'; import css from './menu.css'; type MenuVariant = 'thread-actions' | 'member-actions'; type MenuProps = { +icon: React.Node, +children?: React.Node, +variant?: MenuVariant, +onChange?: boolean => void, }; function Menu(props: MenuProps): React.Node { const buttonRef = React.useRef(); const { renderMenu, setMenuPosition, closeMenu, setCurrentOpenMenu, currentOpenMenu, } = useRenderMenu(); const { icon, children, variant = 'thread-actions', onChange } = props; const ourSymbol = React.useRef(Symbol()); - const menuActionListClasses = React.useMemo( - () => - classnames(css.menuActionList, { - [css.menuActionListThreadActions]: variant === 'thread-actions', - [css.menuActionListMemberActions]: variant === 'member-actions', - }), - [variant], - ); + const menuActionListClasses = classnames(css.menuActionList, { + [css.menuActionListThreadActions]: variant === 'thread-actions', + [css.menuActionListMemberActions]: variant === 'member-actions', + }); const menuActionList = React.useMemo( () => {children}, [children, menuActionListClasses], ); const isOurMenuOpen = currentOpenMenu === ourSymbol.current; const updatePosition = React.useCallback(() => { if (buttonRef.current && isOurMenuOpen) { const { top, left } = buttonRef.current.getBoundingClientRect(); setMenuPosition({ top, left }); } }, [isOurMenuOpen, setMenuPosition]); React.useEffect(() => { if (!window) { return undefined; } window.addEventListener('resize', updatePosition); return () => window.removeEventListener('resize', updatePosition); }, [updatePosition]); React.useEffect(updatePosition, [updatePosition]); const closeMenuCallback = React.useCallback(() => { closeMenu(ourSymbol.current); }, [closeMenu]); React.useEffect(() => { onChange?.(isOurMenuOpen); }, [isOurMenuOpen, onChange]); React.useEffect(() => { if (!isOurMenuOpen) { return undefined; } document.addEventListener('click', closeMenuCallback); return () => { document.removeEventListener('click', closeMenuCallback); }; }, [closeMenuCallback, isOurMenuOpen]); const prevActionListRef = React.useRef(null); React.useEffect(() => { if (!isOurMenuOpen) { prevActionListRef.current = null; return; } if (prevActionListRef.current === menuActionList) { return; } renderMenu(menuActionList); prevActionListRef.current = menuActionList; }, [isOurMenuOpen, menuActionList, renderMenu]); React.useEffect(() => { const ourSymbolValue = ourSymbol.current; return () => closeMenu(ourSymbolValue); }, [closeMenu]); const onClickMenuCallback = React.useCallback(() => { setCurrentOpenMenu(ourSymbol.current); }, [setCurrentOpenMenu]); if (React.Children.count(children) === 0) { return null; } return ( {icon} ); } export default Menu; diff --git a/web/markdown/markdown.react.js b/web/markdown/markdown.react.js index ff3fef445..7b7f37bf1 100644 --- a/web/markdown/markdown.react.js +++ b/web/markdown/markdown.react.js @@ -1,46 +1,42 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import * as SimpleMarkdown from 'simple-markdown'; import css from './markdown.css'; import type { MarkdownRules } from './rules.react'; type Props = { +children: string, +rules: MarkdownRules, }; function Markdown(props: Props): React.Node { const { children, rules } = props; const { simpleMarkdownRules, useDarkStyle } = rules; - const markdownClassName = React.useMemo( - () => - classNames({ - [css.markdown]: true, - [css.darkBackground]: useDarkStyle, - [css.lightBackground]: !useDarkStyle, - }), - [useDarkStyle], - ); + const markdownClassName = classNames({ + [css.markdown]: true, + [css.darkBackground]: useDarkStyle, + [css.lightBackground]: !useDarkStyle, + }); const parser = React.useMemo( () => SimpleMarkdown.parserFor(simpleMarkdownRules), [simpleMarkdownRules], ); const ast = React.useMemo( () => parser(children, { disableAutoBlockNewlines: true }), [parser, children], ); const output = React.useMemo( () => SimpleMarkdown.outputFor(simpleMarkdownRules, 'react'), [simpleMarkdownRules], ); const renderedOutput = React.useMemo(() => output(ast), [ast, output]); return {renderedOutput}; } export default Markdown; diff --git a/web/modals/modal.react.js b/web/modals/modal.react.js index 3f72f142b..7d4b18096 100644 --- a/web/modals/modal.react.js +++ b/web/modals/modal.react.js @@ -1,80 +1,76 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import ModalOverlay from 'lib/components/modal-overlay.react'; import Button from '../components/button.react'; import SWMansionIcon, { type Icon } from '../SWMansionIcon.react'; import css from './modal.css'; export type ModalSize = 'small' | 'large' | 'fit-content'; export type ModalOverridableProps = { +name: string, +icon?: Icon, +onClose: () => void, +withCloseButton?: boolean, +size?: ModalSize, }; type ModalProps = { ...ModalOverridableProps, +children?: React.Node, }; function Modal(props: ModalProps): React.Node { const { size = 'small', children, onClose, name, icon, withCloseButton = true, } = props; - const modalContainerClasses = React.useMemo( - () => - classNames(css.modalContainer, { - [css.modalContainerLarge]: size === 'large', - [css.modalContainerSmall]: size === 'small', - }), - [size], - ); + const modalContainerClasses = classNames(css.modalContainer, { + [css.modalContainerLarge]: size === 'large', + [css.modalContainerSmall]: size === 'small', + }); const cornerCloseButton = React.useMemo(() => { if (!withCloseButton) { return null; } return ( ); }, [onClose, withCloseButton]); const headerIcon = React.useMemo(() => { if (!icon) { return null; } return ; }, [icon]); return ( {headerIcon} {name} {cornerCloseButton} {children} ); } export default Modal; diff --git a/web/settings/relationship/user-list.react.js b/web/settings/relationship/user-list.react.js index 02900fe4b..83265ee51 100644 --- a/web/settings/relationship/user-list.react.js +++ b/web/settings/relationship/user-list.react.js @@ -1,77 +1,73 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import { userStoreSearchIndex as userStoreSearchIndexSelector } from 'lib/selectors/user-selectors'; import type { AccountUserInfo } from 'lib/types/user-types'; import { useSelector } from '../../redux/redux-utils'; import css from './user-list.css'; export type UserRowProps = { +userInfo: AccountUserInfo, +onMenuVisibilityChange?: (visible: boolean) => void, }; type UserListProps = { +userRowComponent: React.ComponentType, +filterUser: (userInfo: AccountUserInfo) => boolean, +usersComparator: (user1: AccountUserInfo, user2: AccountUserInfo) => number, +searchText: string, }; export function UserList(props: UserListProps): React.Node { const { userRowComponent, filterUser, usersComparator, searchText } = props; const userInfos = useSelector(state => state.userStore.userInfos); const userStoreSearchIndex = useSelector(userStoreSearchIndexSelector); const [isMenuVisible, setIsMenuVisible] = React.useState(false); const onMenuVisibilityChange = React.useCallback( (visible: boolean) => setIsMenuVisible(visible), [], ); const searchResult = React.useMemo( () => userStoreSearchIndex.getSearchResults(searchText), [searchText, userStoreSearchIndex], ); const users = React.useMemo(() => { const userIDs = searchText ? searchResult : Object.keys(userInfos); const unfilteredUserInfos = []; for (const id of userIDs) { const { username, relationshipStatus } = userInfos[id]; if (!username) { continue; } unfilteredUserInfos.push({ id, username, relationshipStatus, }); } return unfilteredUserInfos.filter(filterUser).sort(usersComparator); }, [filterUser, searchResult, searchText, userInfos, usersComparator]); const userRows = React.useMemo(() => { const UserRow = userRowComponent; return users.map(user => ( )); }, [userRowComponent, users, onMenuVisibilityChange]); - const containerClasses = React.useMemo( - () => - classNames(css.container, { - [css.noScroll]: isMenuVisible, - }), - [isMenuVisible], - ); + const containerClasses = classNames(css.container, { + [css.noScroll]: isMenuVisible, + }); return {userRows}; }
{name}
{description}
{involvement}
{ancestorPath}